package genedit.model.graph;

import genedit.aStar.Astar;
import genedit.aStar.Path;
import genedit.aStar.SearchMap;
import genedit.controller.interfaceActions.PropertyManager;
import genedit.gui.GMainFrame;
import genedit.model.Workspace;
import genedit.utility.GraphUtil;
import genedit.view.graph.ElementPainter;
import genedit.view.graph.GraphDocumentView;

import java.awt.Point;
import java.awt.Shape;
import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Point2D.Double;
import java.util.ArrayList;

/**
 * Klasa koja predstavlja vezu. SadrÅ¾i podatke o njenom tipu, prelomnim taÄ�kama,
 * kao i njenom poÄ�etku i kraju.
 * 
 * @author Igic
 * @version 1.1 18/12/13
 */

public class Link extends GraphElement {

	private static final long serialVersionUID = 1L;

	/**
	 * Tip linije.
	 */
	protected String lineType;

	/**
	 * Informacija o tome da li se na poÄ�etku veze nalazi strelica ka startnom
	 * elementu.
	 */
	protected boolean startNavigable;

	/**
	 * Informacija o tome da li se na kraju veze nalazi strelica ka zavrÅ¡nom
	 * elementu.
	 */
	protected boolean endNavigable;

	/**
	 * Lista prelomnih taÄ�aka.
	 */
	protected ArrayList<BreakPoint> breakPoints;

	/**
	 * PoÄ�etna konekciona taÄ�na.
	 */
	protected ConnectionPoint start;

	/**
	 * ZavrÅ¡na konekciona taÄ�ka.
	 */
	protected ConnectionPoint end;

	public Link(GraphDocument parent) {
		super(parent);
		breakPoints = new ArrayList<BreakPoint>();
		startNavigable=true;
		endNavigable=true;
	}

	public Link(GraphDocument parent, String name) {
		super(parent, name);
		startNavigable=true;
		endNavigable=true;
	}

	public String getLineType() {
		return lineType;
	}

	public void setLineType(String newLineType) {
		lineType = newLineType;
	}

	public boolean getEndNavigable() {
		return endNavigable;
	}

	public void setEndNavigable(boolean newEndNavigable) {
		endNavigable = newEndNavigable;
	}

	public boolean getStartNavigable() {
		return startNavigable;
	}

	public void setStartNavigable(boolean newStartNavigable) {
		startNavigable = newStartNavigable;
	}

	public ArrayList<BreakPoint> getBreakPoints() {
		if (breakPoints == null)
			breakPoints = new ArrayList<BreakPoint>();
		return breakPoints;
	}

	public java.util.Iterator<BreakPoint> getIteratorBreakPoints() {
		if (breakPoints == null)
			breakPoints = new ArrayList<BreakPoint>();
		return breakPoints.iterator();
	}

	public BreakPoint getLastPoint() {
		return breakPoints.get(breakPoints.size() - 1);
	}

	public void setBreakPoints(java.util.Collection<BreakPoint> newBreakPoints) {
		removeAllBreakPoints();
		for (java.util.Iterator<BreakPoint> iter = newBreakPoints.iterator(); iter.hasNext();)
			addBreakPoints((BreakPoint) iter.next());
	}

	public void addBreakPoints(BreakPoint newBreakPoint) {
		if (newBreakPoint == null)
			return;
		if (this.breakPoints == null)
			this.breakPoints = new ArrayList<BreakPoint>();
		if (!this.breakPoints.contains(newBreakPoint))
			this.breakPoints.add(newBreakPoint);
	}

	public void removeBreakPoints(BreakPoint oldBreakPoint) {
		if (oldBreakPoint == null)
			return;
		if (this.breakPoints != null)
			if (this.breakPoints.contains(oldBreakPoint))
				this.breakPoints.remove(oldBreakPoint);
	}

	public void removeAllBreakPoints() {
		if (breakPoints != null)
			breakPoints.clear();
	}

	public ConnectionPoint getStart() {
		return start;
	}

	public void setStart(ConnectionPoint newConnectionPoint) {

		ConnectionPoint oldConnectionPoint = this.start;
		this.start = null;
		if (oldConnectionPoint != null)
			oldConnectionPoint.removeStarting(this);

		if (newConnectionPoint != null) {
			this.start = newConnectionPoint;
			this.start.addStarting(this);
		}
	}

	public ConnectionPoint getEnd() {
		return end;
	}

	public void setEnd(ConnectionPoint newConnectionPoint) {

		ConnectionPoint oldConnectionPoint = this.end;
		this.end = null;
		if (oldConnectionPoint != null)
			oldConnectionPoint.removeEnding(this);

		if (newConnectionPoint != null) {
			this.end = newConnectionPoint;
			this.end.addEnding(this);
		}

	}

	public ArrayList<genedit.model.graph.Point> getAllPoints() {
		ArrayList<genedit.model.graph.Point> result = new ArrayList<genedit.model.graph.Point>();

		result.add(getStart());
		result.addAll(getBreakPoints());
		genedit.model.graph.Point end = getEnd();
		if (end != null) {
			result.add(end);
		}
		return result;
	}

	/**
	 * pronalazi najkracu mogucu pitanju izmedju 2 elementa a da pri tome ne
	 * sece druge elemente koji nisu veze
	 * 
	 * @param elements
	 */
	public void findPath() {
		int astarDistanceConst=20;
		
		SearchMap map;
		Astar astar;
		map = new SearchMap(Math.abs(this.start.getPosition().getX()
				- this.getEnd().getPosition().getX())
				+ astarDistanceConst
				* Math.max(this.start.getParent().getSize().getWidth(), this.end.getParent()
						.getSize().getWidth()), Math.abs(this.start.getPosition().getY()
				- this.getEnd().getPosition().getY())
				+ astarDistanceConst
				* Math.max(this.start.getParent().getSize().getHeight(), this.end.getParent()
						.getSize().getHeight()), new Point2D.Double(Math.min(this.start
				.getPosition().getX(), this.end.getPosition().getX())
				- Math.max(this.start.getParent().getSize().getWidth(), this.end.getParent()
						.getSize().getWidth()), Math.min(this.start.getPosition().getY(), this.end
				.getPosition().getY())
				- Math.max(this.start.getParent().getSize().getHeight(), this.end.getParent()
						.getSize().getHeight())), this.start.getPosition(), this.end.getPosition());

		astar = new Astar(map);
		Path path = astar.calcShortestPath(this.start.getPosition(), this.end.getPosition());

		if (path != null) {
			this.removeAllBreakPoints();
			this.setBreakPoints(path.getBreakPoints());
			// ovim se vrsi poravnanje, jer zbog aproksimacije prostora zna malo
			// da ukrivi pocetnu i krajnju liniju
			if (!this.breakPoints.isEmpty()) {
				if (Math.abs(((BreakPoint) this.breakPoints.get(0)).getPosition().getX()
						- this.start.getPosition().getX()) < Math
						.abs(((BreakPoint) this.breakPoints.get(0)).getPosition().getY()
								- this.start.getPosition().getY()))
					((BreakPoint) this.breakPoints.get(0)).getPosition().setLocation(
							this.start.getPosition().getX(),
							((BreakPoint) this.breakPoints.get(0)).getPosition().getY());
				else
					((BreakPoint) this.breakPoints.get(0)).getPosition().setLocation(
							((BreakPoint) this.breakPoints.get(0)).getPosition().getX(),
							this.start.getPosition().getY());

				if (Math.abs(((BreakPoint) this.breakPoints.get(breakPoints.size() - 1))
						.getPosition().getX() - this.end.getPosition().getX()) < Math
						.abs(((BreakPoint) this.breakPoints.get(breakPoints.size() - 1))
								.getPosition().getY() - this.end.getPosition().getY()))
					((BreakPoint) this.breakPoints.get(breakPoints.size() - 1)).getPosition()
							.setLocation(
									this.end.getPosition().getX(),
									((BreakPoint) this.breakPoints.get(breakPoints.size() - 1))
											.getPosition().getY());
				else
					((BreakPoint) this.breakPoints.get(breakPoints.size() - 1)).getPosition()
							.setLocation(
									((BreakPoint) this.breakPoints.get(breakPoints.size() - 1))
											.getPosition().getX(), this.end.getPosition().getY());

			}
		}
	}

	public void addIntersectionPoints() {
		ConnectionPoint startingConnection = getStart();
		ConnectionPoint endingConnection = getEnd();
		GraphElement startElement = startingConnection.getParent();
		GraphElement endElement = endingConnection.getParent();
		ArrayList<BreakPoint> breakPoints = getBreakPoints();
		Line2D.Double l1;
		Line2D.Double l2;
		if (getBreakPoints().size() != 0) {
			l1 = new Line2D.Double(startingConnection.getPosition(), breakPoints.get(0)
					.getPosition());
		} else {
			l1 = new Line2D.Double(startingConnection.getPosition(), endingConnection.getPosition());
		}

		if (getBreakPoints().size() != 0) {
			l2 = new Line2D.Double(breakPoints.get(breakPoints.size() - 1).getPosition(),
					endingConnection.getPosition());
		} else {
			l2 = new Line2D.Double(startingConnection.getPosition(), endingConnection.getPosition());
		}

		Point2D.Double intersect = null;
		double[] coordinates = new double[2];
		ArrayList<Point.Double> points = new ArrayList<Point.Double>();
		Shape shape;
		Point.Double point;

		// start
		ElementPainter startElementPainter=GMainFrame.getInstance().getGraphDocumentView(parent).getPainter(startElement);
		shape = startElementPainter.getShape();
		for (PathIterator iter = shape.getPathIterator(null); !iter.isDone(); iter.next()) {
			int type = iter.currentSegment(coordinates);
			if (type == PathIterator.SEG_MOVETO || type == PathIterator.SEG_LINETO) {
				point = new Point.Double(coordinates[0], coordinates[1]);
				GraphUtil.transform(startElementPainter.getTransform(), point);
				points.add(point);
			}
		}
		for (int i = 0; i < points.size() - 1; i++) {
			intersect = getIntersectionPoint(new Line2D.Double(points.get(i), points.get(i + 1)),
					l1);
			if (intersect != null)
				break;
		}
		if (intersect != null)
			breakPoints.add(0, new BreakPoint(intersect));

		// end
		ElementPainter endElementPainter=GMainFrame.getInstance().getGraphDocumentView(parent).getPainter(endElement);
		shape = endElementPainter.getShape();
		points = new ArrayList<Point.Double>();
		for (PathIterator iter = shape.getPathIterator(null); !iter.isDone(); iter.next()) {
			int type = iter.currentSegment(coordinates);
			if (type == PathIterator.SEG_MOVETO || type == PathIterator.SEG_LINETO) {
				point = new Point.Double(coordinates[0], coordinates[1]);
				GraphUtil.transform(endElementPainter.getTransform(), point);
				points.add(point);
			}
		}
		for (int i = 0; i < points.size() - 1; i++) {
			intersect = getIntersectionPoint(new Line2D.Double(points.get(i), points.get(i + 1)),
					l2);
			if (intersect != null)
				break;
		}
		if (intersect != null)
			breakPoints.add(new BreakPoint(intersect));
	}

	public static Point2D.Double getIntersectionPoint(Line2D.Double line1, Line2D.Double line2) {
		if (!line1.intersectsLine(line2))
			return null;
		double px = line1.getX1();
		double py = line1.getY1();
		double rx = line1.getX2() - px;
		double ry = line1.getY2() - py;
		double qx = line2.getX1();
		double qy = line2.getY1();
		double sx = line2.getX2() - qx;
		double sy = line2.getY2() - qy;

		double det = sx * ry - sy * rx;

		if (det == 0) {
			return null;
		} else {
			double z = (sx * (qy - py) + sy * (px - qx)) / det;
			if (z == 0 || z == 1)
				return null;
			return new Point2D.Double(px + z * rx, py + z * ry);
		}
	}

	public void rectLink() {
		if(((Boolean) PropertyManager.getInstance().getProperty(
				PropertyManager.AutoLinkOptimize))==false){
			clearLink();
			return;
		}
				
		ArrayList<genedit.model.graph.Point> points = getAllPoints();
		ArrayList<BreakPoint> newBreakPoints = new ArrayList<BreakPoint>();
		GraphDocumentView activeView = Workspace.getInstance().getActiveDocument();
		
		if(recursiveCheck()){
			return;
		}
		
		if(points.size() > 8)
		{
			findPath();
			clearLink();
			return;
		}
		
		for (int i = 0; i < points.size() - 1; i++) {
			genedit.model.graph.Point p1 = points.get(i);
			genedit.model.graph.Point p2 = points.get(i + 1);

			// ako nije prvi odnosno connectionPoint - a poslednji se svakako ne
			// ispituje
			if (i != 0)
				newBreakPoints.add((BreakPoint) p1);

			if ((p1.getPosition().getX() == p2.getPosition().getX())
					|| (p1.getPosition().getY() == p2.getPosition().getY())) {
				continue;
			}

			// dole levo
			Point2D p = new Point2D.Double(p1.getPosition().getX(), p2.getPosition().getY());
			if (activeView.getElementAtPosition(p) == null) {
				newBreakPoints.add(new BreakPoint(p));
				continue;
			}

			// gore desno
			p = new Point2D.Double(p2.getPosition().getX(), p1.getPosition().getY());
			if (activeView.getElementAtPosition(p) == null) {
				newBreakPoints.add(new BreakPoint(p));
				continue;
			}

			// sredina
			p = new Point2D.Double((p1.getPosition().getX() + p2.getPosition().getX()) / 2, (p1
					.getPosition().getY() + p2.getPosition().getY()) / 2);
			if (activeView.getElementAtPosition(p) == null) {
				Point2D bp1;
				Point2D bp2;
				if (Math.abs(p1.getPosition().getX() - p2.getPosition().getX()) > Math.abs(p1
						.getPosition().getY() - p2.getPosition().getY())) {
					// podeli po x
					bp1 = new Point2D.Double(p.getX(), p1.getPosition().getY());
					bp2 = new Point2D.Double(p.getX(), p2.getPosition().getY());
				} else {
					// podeli po y
					bp1 = new Point2D.Double(p1.getPosition().getX(), p.getY());
					bp2 = new Point2D.Double(p2.getPosition().getX(), p.getY());
				}
				if (Math.abs(bp1.distance(p1.getPosition())) > Math.abs(bp2.distance(p1
						.getPosition()))) {
					// prvo stavi bp2 jer je bliza prvoj
					newBreakPoints.add(new BreakPoint(bp2));
					newBreakPoints.add(new BreakPoint(bp1));
				} else {
					newBreakPoints.add(new BreakPoint(bp1));
					newBreakPoints.add(new BreakPoint(bp2));
				}
				continue;
			}

			// inace idi dole levo
			p = new Point2D.Double(p1.getPosition().getX(), p2.getPosition().getY());
			newBreakPoints.add(new BreakPoint(p));
		}
		setBreakPoints(newBreakPoints);
		clearLink();
	}
	
	boolean recursiveCheck(){
		if(!start.getParent().equals(end.getParent())){
			return false;
		}
		int distanceFromElement=10;
		ArrayList<BreakPoint> newBreakPoints = new ArrayList<BreakPoint>();
		Point2D point;
		GraphElement element=start.getParent();
		
		point=new Point2D.Double(element.getPosition().getX()+element.getSize().getWidth()/2,
				element.getPosition().getY()+element.getSize().getHeight()/2);
		start.setPosition(point);
		
		point=new Point2D.Double(element.getPosition().getX()+element.getSize().getWidth()/2,
				element.getPosition().getY()+element.getSize().getHeight()/2);
		end.setPosition(point);
		
		point=new Point2D.Double(element.getPosition().getX()+element.getSize().getWidth()+distanceFromElement,
				element.getPosition().getY()+element.getSize().getHeight()/2);
		newBreakPoints.add(new BreakPoint(point));
		
		point=new Point2D.Double(element.getPosition().getX()+element.getSize().getWidth()+distanceFromElement,
				element.getPosition().getY()+element.getSize().getHeight()+distanceFromElement);
		newBreakPoints.add(new BreakPoint(point));
		
		point=new Point2D.Double(element.getPosition().getX()+element.getSize().getWidth()/2,
				element.getPosition().getY()+element.getSize().getHeight()+distanceFromElement);
		newBreakPoints.add(new BreakPoint(point));
		
		setBreakPoints(newBreakPoints);
		return true;
	}
	
	public void clearLink() {
		ArrayList<genedit.model.graph.Point> points = getAllPoints();

		if (points.size() < 3) {
			return;
		}
		// brise trivijalno jednake tacke
		for (int i = 1; i < points.size() - 1; i++) {
			genedit.model.graph.Point p1 = points.get(i - 1);
			genedit.model.graph.Point p2 = points.get(i);
			genedit.model.graph.Point p3 = points.get(i + 1);

			if ((p1.getPosition().getX() == p2.getPosition().getX())
					&& (p2.getPosition().getX() == p3.getPosition().getX())) {
				removeBreakPoints((BreakPoint) p2);
				points.remove(i);
				i--;
			}

			if ((p1.getPosition().getY() == p2.getPosition().getY())
					&& (p2.getPosition().getY() == p3.getPosition().getY())) {
				removeBreakPoints((BreakPoint) p2);
				points.remove(i);
				i--;
			}
		}

		// smanjuje broj tacaka
		if (points.size() < 3) {
			return;
		}
		double treshold = 20;
		for (int i = 1; i < points.size() - 1; i++) {
			genedit.model.graph.Point p1 = points.get(i - 1);
			genedit.model.graph.Point p2 = points.get(i);
			genedit.model.graph.Point p3 = points.get(i + 1);

			if (Math.abs(p1.getPosition().distance(p2.getPosition())) < treshold) {
				if (p2.getPosition().getX() == p3.getPosition().getX()) {
					p1.setPosition(new Point2D.Double(p1.getPosition().getX(), p2.getPosition()
							.getY()));
				} else {
					p1.setPosition(new Point2D.Double(p2.getPosition().getX(), p1.getPosition()
							.getY()));
				}
			}
		}

		clearStartAndEnd();
	}

	/**
	 * Obezbedjuje da nema vise od jedne tacke u okviru pocetnog i krajnjeg
	 * elementa
	 * */
	public void clearStartAndEnd() {
		// clear start
		ArrayList<BreakPoint> breakPoints = getBreakPoints();		
		ElementPainter startElementPainter=GMainFrame.getInstance()
				.getGraphDocumentView(parent).getPainter(getStart().getParent());
		for (int i = 0; i < breakPoints.size(); i++) {
			if (startElementPainter.isElementAt(breakPoints.get(i).getPosition())) {
				getStart().setPosition(breakPoints.get(i).getPosition());
				breakPoints.remove(i);
				i--;
			}
		}

		// clear end
		ElementPainter endElementPainter=GMainFrame.getInstance()
				.getGraphDocumentView(parent).getPainter(getEnd().getParent());
		for (int i = breakPoints.size() - 1; i >= 0; i--) {
			if (endElementPainter.isElementAt(breakPoints.get(i).getPosition())) {
				getEnd().setPosition(breakPoints.get(i).getPosition());
				breakPoints.remove(i);
			}
		}
	}

	public void removeIntersectPoints() {
		if (breakPoints.size() < 2)
			return;
		breakPoints.remove(0);
		breakPoints.remove(breakPoints.size() - 1);
		ElementPainter startP = GMainFrame.getInstance()
				.getGraphDocumentView(parent).getPainter(start.getParent());
		ElementPainter endP = GMainFrame.getInstance()
				.getGraphDocumentView(parent).getPainter(end.getParent());

		for (int i = 0; i < breakPoints.size(); i++) {
			if (startP.isElementAt(breakPoints.get(i).getPosition())
					|| endP.isElementAt(breakPoints.get(i).getPosition())) {
				breakPoints.remove(i);
				i--;
				continue;
			}
		}
	}

	/**
	 * vraca indeks selektovanog breakpointa ili -1 ako nije ni jedan
	 * 
	 * @param pos
	 * @return
	 */
	public int breakPointAt(Point2D pos) {
		ArrayList<BreakPoint> points = getBreakPoints();
		for (int i = 0; i < points.size(); i++) {
			int distance = (int) Point2D.Double.distance(points.get(i).getPosition().getX(), points
					.get(i).getPosition().getY(), pos.getX(), pos.getY());
			if (distance < 5)
				return i;
		}
		return -1;
	}
}